package aceim.app.utils.history.impl; import java.io.BufferedOutputStream; import java.io.BufferedReader; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.RandomAccessFile; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.Executors; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import aceim.api.dataentity.Buddy; import aceim.api.dataentity.FileInfo; import aceim.api.dataentity.FileMessage; import aceim.api.dataentity.Message; import aceim.api.dataentity.ServiceMessage; import aceim.api.dataentity.TextMessage; import aceim.api.dataentity.tkv.MessageAttachment; import aceim.api.dataentity.tkv.MessageAttachment.MessageAttachmentType; import aceim.api.utils.Logger; import aceim.app.dataentity.Account; import aceim.app.utils.history.HistorySaver; import android.content.Context; public final class JsonHistorySaver implements HistorySaver { private static final String CHARSET = "UTF-16BE"; private static final byte[] RECORD_DIVIDER_BYTES; private static final String RECORD_DIVIDER = "{}"; private static final byte[] NEW_LINE_BYTES; private static final int MAX_MESSAGES_TO_READ = 8; public static final String FILE_EXTENSION = ".history"; private final Context mContext; static { RECORD_DIVIDER_BYTES = getBytes(RECORD_DIVIDER); NEW_LINE_BYTES = getBytes("\n"); } public JsonHistorySaver(Context context) { this.mContext = context; } /* (non-Javadoc) * @see aceim.app.utils.HistorySaver#saveMessage(aceim.app.api.dataentity.Buddy, aceim.app.api.dataentity.Message) */ @Override public void saveMessage(final Buddy buddy, final Message message) { Runnable r = new Runnable() { @Override public void run() { OutputStream stream; try { stream = new BufferedOutputStream(mContext.openFileOutput(buddy.getFilename() + FILE_EXTENSION, Context.MODE_APPEND)); } catch (FileNotFoundException e1) { // Should not happen, as Android API guarantees the creation of the new file. Logger.log("Starting new history file for " + buddy); stream = null; } JSONObject o; try { o = HistoryObject.fromMessage(message); } catch (JSONException e) { Logger.log(e); return; } byte[] buffer = getBytes(o.toString()); try { stream.write(RECORD_DIVIDER_BYTES); stream.write(NEW_LINE_BYTES); stream.write(buffer); stream.write(NEW_LINE_BYTES); } catch (IOException e) { Logger.log(e); } finally { try { stream.flush(); stream.close(); } catch (IOException e) { Logger.log(e); } } } }; Executors.defaultThreadFactory().newThread(r).start(); } /* (non-Javadoc) * @see aceim.app.utils.HistorySaver#getMessages(aceim.app.api.dataentity.Buddy) */ @Override public List<Message> getMessages(Buddy buddy) { return getMessages(buddy, 0, MAX_MESSAGES_TO_READ); } /* (non-Javadoc) * @see aceim.app.utils.HistorySaver#getMessages(aceim.app.api.dataentity.Buddy, int, int) */ @Override public List<Message> getMessages(Buddy buddy, int startFrom, int maxMessagesToRead) { BufferedReader stream = null; try { stream = new BufferedReader(new InputStreamReader(new ReverseLineInputStream(mContext.getFileStreamPath(buddy.getFilename() + FILE_EXTENSION)), CHARSET)); int index = 0; String line = null; StringBuilder sb = new StringBuilder(); List<Message> messages = new ArrayList<Message>(); long endTo = startFrom + maxMessagesToRead; while ((line = stream.readLine()) != null && index < endTo) { if (line.equals(RECORD_DIVIDER)) { if (sb.length() > 2) { if (index >= startFrom) { try { HistoryObject o = new HistoryObject(sb.toString()); messages.add(0, o.toMessage(buddy)); } catch (JSONException e) { Logger.log(e); endTo++; } } } index++; } else { if (index >= startFrom) { sb.insert(0, line); } } } return messages; } catch (FileNotFoundException e) { Logger.log("No history found for " + buddy); } catch (IOException e) { Logger.log(e); } finally { if (stream != null) { try { stream.close(); } catch (IOException e) { Logger.log(e); } } } return Collections.emptyList(); } private static final byte[] getBytes(String string) { try { return string.getBytes(CHARSET); } catch (UnsupportedEncodingException e) { Logger.log(CHARSET + " is unsupported, using " + Charset.defaultCharset() + " instead"); return string.getBytes(); } } private static enum MessageType { TEXT, FILE, SERVICE; static MessageType fromMessage(Message message) { if (message instanceof TextMessage) { return MessageType.TEXT; } else if (message instanceof FileMessage) { return MessageType.FILE; } else if (message instanceof ServiceMessage) { return MessageType.SERVICE; } else { // TODO here may be dragons... throw new IllegalArgumentException("Cannot store message of type " + message.getClass().getName()); } } Message getMessageByType(Buddy buddy) { Message message; switch (this) { case FILE: message = new FileMessage(buddy.getServiceId(), buddy.getProtocolUid(), new ArrayList<FileInfo>(0)); break; case SERVICE: message = new ServiceMessage(buddy.getServiceId(), buddy.getProtocolUid(), false); break; case TEXT: message = new TextMessage(buddy.getServiceId(), buddy.getProtocolUid()); break; default: message = null; break; } return message; } } private static final class HistoryObject extends JSONObject { private HistoryObject(){ super(); } private HistoryObject(String str) throws JSONException { super(str); } static HistoryObject fromMessage(Message message) throws JSONException{ HistoryObject o = new HistoryObject(); o.put("type", MessageType.fromMessage(message).name()); o.put("contact-name", message.getContactDetail() != null ? message.getContactDetail() : message.getContactUid()); o.put("incoming", message.isIncoming()); o.put("text", message.getText()); o.put("message-id", message.getMessageId()); o.put("time", message.getTime()); if (message instanceof TextMessage) { JSONArray array = new JSONArray(); for (MessageAttachment a : ((TextMessage)message).getAttachments()) { JSONObject ao = new JSONObject(); ao.put("type", a.getType().name()); ao.put("src", a.getSource()); if (a.getTitle() != null) { ao.put("title", a.getTitle()); } array.put(ao); } o.put("attachments", array); } return o; } Message toMessage(Buddy buddy) throws JSONException { MessageType type = MessageType.valueOf((String) get("type")); String contactName = getString("contact-name"); boolean incoming = getBoolean("incoming"); String text = getString("text"); long messageId = getLong("message-id"); long time = getLong("time"); Message m = type.getMessageByType(buddy); if (!contactName.equals(buddy.getProtocolUid())) { m.setContactDetail(contactName); } m.setIncoming(incoming); m.setText(text); m.setMessageId(messageId); m.setTime(time); if (m instanceof TextMessage) { JSONArray array = optJSONArray("attachments"); if (array != null) { for (int i=0; i<array.length(); i++) { JSONObject o = array.optJSONObject(i); if (o != null) { MessageAttachmentType atype = MessageAttachmentType.valueOf(o.getString("type")); MessageAttachment a = new MessageAttachment(atype, o.optString("title"), o.optString("src")); ((TextMessage)m).getAttachments().add(a); } } } } return m; } } private static class ReverseLineInputStream extends InputStream { RandomAccessFile in; long currentLineStart = -1; long currentLineEnd = -1; long currentPos = -1; long lastPosInFile = -1; ReverseLineInputStream(File file) throws FileNotFoundException { in = new RandomAccessFile(file, "r"); currentLineStart = file.length(); currentLineEnd = file.length(); lastPosInFile = file.length() -1; currentPos = currentLineEnd; } public void findPrevLine() throws IOException { currentLineEnd = currentLineStart; // There are no more lines, since we are at the beginning of the file and no lines. if (currentLineEnd == 0) { currentLineEnd = -1; currentLineStart = -1; currentPos = -1; return; } long filePointer = currentLineStart -1; while ( true) { filePointer--; // we are at start of file so this is the first line in the file. if (filePointer < 0) { break; } in.seek(filePointer); int readByte = in.readByte(); // We ignore last LF in file. search back to find the previous LF. if (readByte == 0xA && filePointer != lastPosInFile ) { break; } } // we want to start at pointer +1 so we are after the LF we found or at 0 the start of the file. currentLineStart = filePointer + 1; currentPos = currentLineStart; } public int read() throws IOException { if (currentPos < currentLineEnd ) { in.seek(currentPos++); int readByte = in.readByte(); return readByte; } else if (currentPos < 0) { return -1; } else { findPrevLine(); return read(); } } } @Override public boolean deleteHistory(Buddy buddy) { return mContext.deleteFile(buddy.getFilename() + FILE_EXTENSION); } @Override public void removeAccount(Account account) { if (account == null) return; for (Buddy buddy : account.getBuddyList()) { deleteHistory(buddy); } } }